/*
 * Copyright (C) 2012 CyberAgent
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.acenodie.acenodieopenglturorial.filter;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.LinkedList;

import android.graphics.Bitmap;
import android.graphics.PointF;
import android.opengl.GLES20;

public class GPUImageFilter {
	public static final String NO_FILTER_VERTEX_SHADER = "" + "attribute vec4 position;\n"
			+ "attribute vec4 inputTextureCoordinate;\n" + " \n" + "varying vec2 textureCoordinate;\n" + " \n"
			+ "void main()\n" + "{\n" + "    gl_Position = position;\n"
			+ "    textureCoordinate = inputTextureCoordinate.xy;\n" + "}";
	public static final String NO_FILTER_FRAGMENT_SHADER = "" + "varying highp vec2 textureCoordinate;\n" + " \n"
			+ "uniform sampler2D inputImageTexture;\n" + " \n" + "void main()\n" + "{\n"
			+ "     gl_FragColor = texture2D(inputImageTexture, textureCoordinate);\n" + "}";

	private final LinkedList<Runnable> mRunOnDraw;
	private final String mVertexShader;
	private final String mFragmentShader;
	protected int mGLProgId;
	protected int mGLAttribPosition;
	protected int mGLUniformTexture;
	protected int mGLAttribTextureCoordinate;
	protected int mGLStrengthLocation;
	protected int mOutputWidth;
	protected int mOutputHeight;
	protected boolean mIsInitialized;
	protected FloatBuffer mGLCubeBuffer;
	protected FloatBuffer mGLTextureBuffer;
	protected int mSurfaceWidth, mSurfaceHeight;
	
	protected int[] mFrameBuffers = null;
	protected int[] mFrameBufferTextures = null;
	protected int mFrameWidth = -1;
	protected int mFrameHeight = -1;

	public GPUImageFilter() {
		this(NO_FILTER_VERTEX_SHADER, NO_FILTER_FRAGMENT_SHADER);
	}

	public GPUImageFilter(final String vertexShader, final String fragmentShader) {
		mRunOnDraw = new LinkedList<Runnable>();
		mVertexShader = vertexShader;
		mFragmentShader = fragmentShader;

		mGLCubeBuffer = ByteBuffer.allocateDirect(TextureRotationUtil.CUBE.length * 4).order(ByteOrder.nativeOrder())
				.asFloatBuffer();
		mGLCubeBuffer.put(TextureRotationUtil.CUBE).position(0);

		mGLTextureBuffer = ByteBuffer.allocateDirect(TextureRotationUtil.TEXTURE_NO_ROTATION.length * 4)
				.order(ByteOrder.nativeOrder()).asFloatBuffer();
		mGLTextureBuffer.put(TextureRotationUtil.getRotation(Rotation.NORMAL, false, true)).position(0);
	}

	public void init() {
		onInit();
//		mIsInitialized = true;
		onInitialized();
	}

	protected void onInit() {
		mGLProgId = OpenGLUtils.loadProgram(mVertexShader, mFragmentShader);
		if (mGLProgId == 0) {
			mIsInitialized = false;
		} else {
			mIsInitialized = true;
		}
		mGLAttribPosition = GLES20.glGetAttribLocation(mGLProgId, "position");
		mGLUniformTexture = GLES20.glGetUniformLocation(mGLProgId, "inputImageTexture");
		mGLAttribTextureCoordinate = GLES20.glGetAttribLocation(mGLProgId, "inputTextureCoordinate");
		mGLStrengthLocation = GLES20.glGetUniformLocation(mGLProgId, "strength");
	}

	protected void onInitialized() {
		setFloat(mGLStrengthLocation, 1.0f);
	}

	public final void destroy() {
		mIsInitialized = false;
		GLES20.glDeleteProgram(mGLProgId);
		onDestroy();
	}

	protected void onDestroy() {
		destroyFramebuffers();
	}

	public void onOutputSizeChanged(final int width, final int height) {
		mOutputWidth = width;
		mOutputHeight = height;
		initFrameBuffer(width, height);
	}

	public int onDrawFrame(final int textureId, final FloatBuffer cubeBuffer, final FloatBuffer textureBuffer) {
		if (mFrameBuffers == null || mFrameBufferTextures == null) {
            return OpenGLUtils.NOT_INIT;
        }
		
		GLES20.glViewport(0, 0, mOutputWidth, mOutputHeight);
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]);
        GLES20.glClearColor(0, 0, 0, 0);
		
		GLES20.glUseProgram(mGLProgId);
		runPendingOnDrawTasks();
		if (!mIsInitialized) {
			return OpenGLUtils.NOT_INIT;
		}

		cubeBuffer.position(0);
		GLES20.glVertexAttribPointer(mGLAttribPosition, 2, GLES20.GL_FLOAT, false, 0, cubeBuffer);
		GLES20.glEnableVertexAttribArray(mGLAttribPosition);
		textureBuffer.position(0);
		GLES20.glVertexAttribPointer(mGLAttribTextureCoordinate, 2, GLES20.GL_FLOAT, false, 0, textureBuffer);
		GLES20.glEnableVertexAttribArray(mGLAttribTextureCoordinate);
		if (textureId != OpenGLUtils.NO_TEXTURE) {
			GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
			GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
			GLES20.glUniform1i(mGLUniformTexture, 0);
		}
		onDrawArraysPre();
		GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
		GLES20.glDisableVertexAttribArray(mGLAttribPosition);
		GLES20.glDisableVertexAttribArray(mGLAttribTextureCoordinate);
		onDrawArraysAfter();
		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
		
		GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
		return mFrameBufferTextures[0];
//		return OpenGLUtils.ON_DRAWN;
	}

	public int onDrawFrame(final int textureId) {
		GLES20.glUseProgram(mGLProgId);
		runPendingOnDrawTasks();
		if (!mIsInitialized)
			return OpenGLUtils.NOT_INIT;

		mGLCubeBuffer.position(0);
		GLES20.glVertexAttribPointer(mGLAttribPosition, 2, GLES20.GL_FLOAT, false, 0, mGLCubeBuffer);
		GLES20.glEnableVertexAttribArray(mGLAttribPosition);
		mGLTextureBuffer.position(0);
		GLES20.glVertexAttribPointer(mGLAttribTextureCoordinate, 2, GLES20.GL_FLOAT, false, 0, mGLTextureBuffer);
		GLES20.glEnableVertexAttribArray(mGLAttribTextureCoordinate);
		if (textureId != OpenGLUtils.NO_TEXTURE) {
			GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
			GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
			GLES20.glUniform1i(mGLUniformTexture, 0);
		}
		onDrawArraysPre();
		GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
		GLES20.glDisableVertexAttribArray(mGLAttribPosition);
		GLES20.glDisableVertexAttribArray(mGLAttribTextureCoordinate);
		onDrawArraysAfter();
		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
		return OpenGLUtils.ON_DRAWN;
	}

	protected void onDrawArraysPre() {
	}

	protected void onDrawArraysAfter() {
	}

	protected void runPendingOnDrawTasks() {
		while (!mRunOnDraw.isEmpty()) {
			mRunOnDraw.removeFirst().run();
		}
	}
	
	public void setInitialized(boolean initialized) {
		mIsInitialized = initialized;
	}

	public boolean isInitialized() {
		return mIsInitialized;
	}

	public int getOutputWidth() {
		return mOutputWidth;
	}

	public int getOutputHeight() {
		return mOutputHeight;
	}

	public int getProgram() {
		return mGLProgId;
	}

	public int getAttribPosition() {
		return mGLAttribPosition;
	}

	public int getAttribTextureCoordinate() {
		return mGLAttribTextureCoordinate;
	}

	public int getUniformTexture() {
		return mGLUniformTexture;
	}
	
	public void setBitmap(final Bitmap bitmap) {
		
	}
	
	public void setRotation(final Rotation rotation, final boolean flipHorizontal, final boolean flipVertical) {
		
	}
	
	public void setLevel(int paramInt) {
		
	}

	protected void setInteger(final int location, final int intValue) {
		runOnDraw(new Runnable() {
			@Override
			public void run() {
				GLES20.glUniform1i(location, intValue);
			}
		});
	}

	protected void setFloat(final int location, final float floatValue) {
		runOnDraw(new Runnable() {
			@Override
			public void run() {
				GLES20.glUniform1f(location, floatValue);
			}
		});
	}

	protected void setFloatVec2(final int location, final float[] arrayValue) {
		runOnDraw(new Runnable() {
			@Override
			public void run() {
				GLES20.glUniform2fv(location, 1, FloatBuffer.wrap(arrayValue));
			}
		});
	}

	protected void setFloatVec3(final int location, final float[] arrayValue) {
		runOnDraw(new Runnable() {
			@Override
			public void run() {
				GLES20.glUniform3fv(location, 1, FloatBuffer.wrap(arrayValue));
			}
		});
	}

	protected void setFloatVec4(final int location, final float[] arrayValue) {
		runOnDraw(new Runnable() {
			@Override
			public void run() {
				GLES20.glUniform4fv(location, 1, FloatBuffer.wrap(arrayValue));
			}
		});
	}

	protected void setFloatArray(final int location, final float[] arrayValue) {
		runOnDraw(new Runnable() {
			@Override
			public void run() {
				GLES20.glUniform1fv(location, arrayValue.length, FloatBuffer.wrap(arrayValue));
			}
		});
	}

	protected void setPoint(final int location, final PointF point) {
		runOnDraw(new Runnable() {

			@Override
			public void run() {
				float[] vec2 = new float[2];
				vec2[0] = point.x;
				vec2[1] = point.y;
				GLES20.glUniform2fv(location, 1, vec2, 0);
			}
		});
	}

	protected void setUniformMatrix3f(final int location, final float[] matrix) {
		runOnDraw(new Runnable() {

			@Override
			public void run() {
				GLES20.glUniformMatrix3fv(location, 1, false, matrix, 0);
			}
		});
	}

	protected void setUniformMatrix4f(final int location, final float[] matrix) {
		runOnDraw(new Runnable() {

			@Override
			public void run() {
				GLES20.glUniformMatrix4fv(location, 1, false, matrix, 0);
			}
		});
	}

	protected void runOnDraw(final Runnable runnable) {
		synchronized (mRunOnDraw) {
			mRunOnDraw.addLast(runnable);
		}
	}

	public void onDisplaySizeChanged(final int width, final int height) {
		mSurfaceWidth = width;
		mSurfaceHeight = height;
	}
	
	public void initFrameBuffer(int width, int height) {
		if (mFrameBuffers != null && (mFrameWidth != width || mFrameHeight != height)) {
			destroyFramebuffers();
		}
		
		if (mFrameBuffers == null) {
			mFrameWidth = width;
			mFrameHeight = height;
			mFrameBuffers = new int[1];
			mFrameBufferTextures = new int[1];

			GLES20.glGenFramebuffers(1, mFrameBuffers, 0);

			GLES20.glGenTextures(1, mFrameBufferTextures, 0);
			GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mFrameBufferTextures[0]);
			GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0,
					GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
			GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
					GLES20.GL_LINEAR);
			GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
					GLES20.GL_LINEAR);
			GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
					GLES20.GL_CLAMP_TO_EDGE);
			GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
					GLES20.GL_CLAMP_TO_EDGE);

			GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]);
			GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
					GLES20.GL_TEXTURE_2D, mFrameBufferTextures[0], 0);

			GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
			GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
		}
	}

	public void destroyFramebuffers() {
		if (mFrameBufferTextures != null) {
			GLES20.glDeleteTextures(1, mFrameBufferTextures, 0);
			mFrameBufferTextures = null;
		}
		if (mFrameBuffers != null) {
			GLES20.glDeleteFramebuffers(1, mFrameBuffers, 0);
			mFrameBuffers = null;
		}
		mFrameWidth = -1;
		mFrameHeight = -1;
	}
}
